函數式程式設計(Functional Programming)是一種設計模式(Design pattern),主程式能夠將函數當作參數,進行傳遞(輸出/輸入參數)及評估。使用Functional Programming的好處如下:
可實作Functional Programming的語言必須支援First-class function,意思是函數可被視為普通數值,可被儲存於變數,可被其他函數當作參數傳遞並被執行,Python完全符合這些要求。以下我們就從頭開發(From scratch),展現Functional Programming的威力。
範例1. 實作加減乘除四則運算,檔案名稱為fp1.py。
def add(a, b):
return a+b
def subtract(a, b):
return a-b
def multiply(a, b):
return a*b
def divide(a, b):
return a/b
subtract(5,10)
執行結果無誤:-5。
加寫一個函數perform1,可接受加減乘除函數作為輸入參數。
def perform1(func, a, b ):
return func(a, b)
print(perform1(subtract, 5, 10))
這樣有甚麼好處呢? 假設有一天我們想要擴充功能,希望程式也能夠進行指數及log運算,這時原先的程式都不必修改,只要加指數及log運算函數即可。
import math
...
def exp(a, b):
return a**b
def log(a, b):
return math.log(a, b)
print(perform1(log, 100, 10))
範例2. 程式也可以將參數a、b改為*arg,可以接受任意個參數,與print函數類似,參看fp2.py。
def add(*arg):
total = 0
for i in arg:
total += i
return total
def multiply(*arg):
total = 1
for i in arg:
total *= i
return total
def perform1(func, *arg):
return func(*arg)
if __name__ == "__main__":
print(perform1(multiply, 1, 10, 20))
執行結果無誤:200。
Python內建許多高階函數,例如上一篇談到的map、filter、reduce,另外,許多套件也建構高階函數,供開發者使用,例如Pandas的apply、applymap...等。
範例3. 使用reduce實作範例2功能。
from functools import reduce
# 測試資料
numbers = (1, 2, 3, 4)
# 連加
result = reduce(lambda x, y: x + y, numbers)
print(result)
執行結果無誤:10。
實作連乘函數:Lambda function改為乘號(*)即可。
# 連乘
result = reduce(lambda x, y: x * y, numbers)
print(result)
匿名函數(Lambda function)是沒有名稱的函數,語法較簡潔,同時也有利於將函數以參數傳遞,例如【Python錦囊㊙️技2】的小型計算機程式,在指定button的事件處理時,可以使用下列語法:
# 建立按鈕
button1 = Button(app, text=' 1 ', fg='black', bg='lightblue',
command=lambda: press(1), height=1, width=7)
command屬性指定button的事件處理函數為press(1),若刪除【lambda:】,程式會在建立按鈕時,就馬上執行press(1)。
以下改寫add函數為匿名函數。
def add(a, b):
return a+b
改為:
add = lambda a, b: a+b
範例4. 使用匿名函數改寫fp1.py,另存為fp_lambda.py。
import math
add = lambda a, b: a+b
subtract = lambda a, b: a-b
multiply = lambda a, b: a*b
divide = lambda a, b: a/b
exp = lambda a, b: a**b
log = lambda a, b: math.log(a, b)
def perform1(func, a, b ):
return func(a, b)
if __name__ == "__main__":
# print(subtract(5, 10))
print(perform1(subtract, 5, 10))
print(perform1(log, 100, 10))
如果函數只使用一次,也可以直接在呼叫時直接撰寫匿名函數,不須事先定義。
print(perform1(lambda a, b: math.log(a, b), 1000, 10))
範例5. Python提供內建模組operator,對常用的運算都已定義成函數,可直接使用,可參閱Python官方文件,如下圖:
測試如下:
from operator import add, sub, mul, truediv
def perform1(func, a, b ):
return func(a, b)
print(perform1(add, 100, 10))
執行結果:110。
前面講過Python支援First-class function,函數可被儲存,之後可隨時取用。
範例6. 函數儲存與載入。
from operator import add, sub, mul, truediv
import joblib
# 建立+-*/對應add, sub, mul, truediv函數的字典
operators = list('+-*/')
func_list = [add, sub, mul, truediv]
operators_dict = {operators[i]: func_list[i] for i in range(len(operators))}
# 序列化(Serialization)存檔
joblib.dump(operators_dict, 'operator.joblib')
# 反序列化(Deserialization),自檔案載入
operators_dict2 = joblib.load('operator.joblib')
def perform_operation(op_string, a, b):
return operators_dict2[op_string](a, b)
print(perform_operation('+', 100, 10))
perform_operation('+', 100, 10)
有兩個方面可以延伸:
表格處理套件Pandas也提供許多高階函數(HOF),例如apply,用於轉換欄位值,以至證交所網站下載股價為例,通常要整理成可分析的表格,需要費一番手腳。
範例7. 每日收盤行情下載,檔案為【每日收盤行情下載.ipynb】,節錄重要程式如下:
# 引進相關套件
import requests
from io import StringIO
import pandas as pd
import numpy as np
# 資料日期
date1 = '20240913'
# 網址
url= 'http://www.twse.com.tw/exchangeReport/MI_INDEX?response=csv&date={}&type=ALL'.format(date1)
# 送出要求,並取得回應資料
response = requests.get(url)
clean_data=[]
for row in response.text.split('\n'):
fields=row.split('",')
if len(fields) == 17 and row[0] != '=':
clean_data.append(row.replace(' ',''))
csv_data = "\n".join(clean_data)
df = pd.read_csv(StringIO(csv_data))
執行結果:
# 計算漲跌價差
df["漲跌價差"] = df.apply(lambda r: 0-r["漲跌價差"] if r["漲跌(+/-)"] =='-' else r["漲跌價差"], axis=1)
numeric_columns=['成交股數','成交筆數','成交金額','開盤價','最高價','最低價','收盤價', '本益比']
for i in numeric_columns:
df[i]=df[i].map(lambda x:x.replace(',', '').replace('--', '')) # 去除三位一撇、--
df[i]=pd.to_numeric(df[i])
執行結果:
透過Pandas高階函數可以很輕易地整理好表格資料。
被呼叫的函數最好是純函數(Pure function),具備以下特性,以避免產生副作用(side effects):
範例8. 純函數(Pure function) vs. 不純函數(Impure function),不純函數使用了全局變數 strip_impure_call_count。
# 純函數(Pure function)
def strip_pure(sentence: str) -> str:
return sentence.strip()
# 不純函數(Impure function)
strip_impure_call_count = 0
def strip_impure(sentence: str) -> str:
global strip_impure_call_count
stripped_sentence = sentence.strip()
# Side effect 1: 使用全局變數 strip_impure_call_count
strip_impure_call_count += 1
# Side effect 2: 輸出至螢幕(stdout)
print(f"Called strip_impure {strip_call_count} times")
return stripped_sentence
註:程式碼來自【Functional Programming in Python】。
從以上的實作,我們大致上知道Functional Programming可以讓系統架構變得非常有彈性,擴充功能,不需修改既有的程式碼,對於已上線的系統且需求變更持續頻繁的狀況,Functional Programming架構可以保持維運的穩定性,因為既有的程式碼未異動,所以也不需重新測試,只要測試新增的功能,對於中大系統開發而言,簡直是一大福音。
下次我們進一步應用Functional Programming及AST(Abstract Syntax Tree)製作一個數學式評估器(Mathematical evaluator)以及更複雜的程式解析器(Parser),敬請期待嘍。
本系列的程式碼會統一放在GitHub,本篇的程式放在src/4資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。